<?php
/**
 * OAuth_Uri
 * This class represents a URI. Rather than picking apart URIs with parse_url
 * and similar functions throughout the ilbrary, I've just put all of that kind
 * of functionality into this class.
 *
 * Eventually I'll probably move this class and the request/response classes
 * into another namespace (like Http or something).
 * 
 * @package OAuth
 * @copyright Luke Visinoni (luke.visinoni@gmail.com)
 * @author Luke Visinoni (luke.visinoni@gmail.com)
 * @license GNU Lesser General Public License
 */
class OAuth_Uri {

    /**
     * @var string The scheme for this uri
     */
    protected $scheme = "http";
    
    /**
     * @var string The host name (www.example.com)
     */
    protected $host;
    
    /**
     * @var integer The port number. If scheme is http and port is 80, this is
     * left out of the resulting URI string. If scheme is https and port is 443,
     * this is left out of the resulting URI string.
     */
    protected $port;
    
    /**
     * @var string The username
     */
    protected $user;
    
    /**
     * @var string The password
     */
    protected $pass;
    
    /**
     * @var string The path after the host
     */
    protected $path;
    
    /**
     * @var string The path info after the file name
     */
    protected $pathinfo;
    
    /**
     * @var array The query parameters
     */
    protected $params = array();
    
    /**
     * This should be called something else but I can't remember what...
     * @var string This is the part after # in the URI
     */
    protected $fragment;
    
    /**
     * Class constructor
     * For now, this class only supports http and https.
     * Eventually it may support ftp, mailto, etc.
     */
    public function __construct($scheme, $host = null, $path = null, $params = null, $port = null, $pathinfo = null, $fragment = null, $user = null, $password = null) {
    
        $this->setScheme($scheme)
            ->setPort($port)
            ->setHostName($host)
            ->setPath($path)
            ->setParams($params)
            ->setPathInfo($pathinfo);
        
        // if user was passed in, set the credentials
        if (!is_null($user)) {
            if (!is_null($password)) {
                $this->setCredentials($user, $password);
            } else {
                // @todo Find out if it is ok to set a user without a password...
                $this->setCredentials($user);
            }
        }
    
    }
    
    public function setScheme($scheme) {
    
        switch ($scheme) {
            case "http":
                if (!$this->getPort()) $this->port = 80;
            case "https":
                if (!$this->getPort()) $this->port = 443;
            default:
                $this->scheme = (string) $scheme;
                break;
        }
        return $this;
    
    }
    
    public function getScheme() {
    
        return $this->scheme;
    
    }
    
    public function setPort($port = null) {
    
        if (!is_null($port)) {
            $this->port = (integer) $port;
        }
        return $this;
    
    }
    
    public function getPort() {
    
        return $this->port;
    
    }
    
    public function setHostName($host) {
    
        $this->host = (string) $host;
        return $this;
    
    }
    
    public function getHostName() {
    
        return $this->host;
    
    }
    
    public function setPath($path) {
    
        $this->path = (string) $path;
        return $this;
    
    }
    
    public function getPath() {
    
        return $this->path;
    
    }
    
    public function setParams($params) {
    
        if (is_string($params)) {
            $params = ltrim($params, '?');
            $parts = explode('&', $params);
            $params = array();
            foreach ($parts as $part) {
                $part = trim($part);
                if (empty($part)) continue;
                if (!strpos($part, '=')) {
                    $key = $part;
                    $val = null;
                } else {
                    list($key, $val) = explode('=', $part);
                }
                $params[$key] = $val;
            }
        }
        $this->params = $params;
        return $this;
    
    }
    
    public function getParams() {
    
        return $this->params;
    
    }
    
    // @todo cast to string?
    public function setParam($key, $val) {
    
        $this->params[$key] = $val;
    
    }
    
    public function getParam($key) {
    
        if (isset($this->params[$key])) return $this->params[$key];
        throw new OAuth_Uri_Exception_ParamNotExist("The param you have requested does not exist: '$key'");
    
    }
    
    /**
     * Because there is no need for a password without a username,
     * they need to be set at the same time using this method.
     */
    public function setCredentials($user, $password = null) {
    
        $this->user = (string) $user;
        if (!is_null($password)) {
            $this->pass = $password;
        }
        return $this;
    
    }
    
    public function getPassword() {
    
        return $this->pass;
    
    }
    
    public function getUser() {
    
        return $this->user;
    
    }
    
    public function setPathInfo($pathinfo) {
    
        $this->pathinfo = $pathinfo;
        return $this;
    
    }
    
    public function getPathInfo() {
    
        return $this->pathinfo;
    
    }
    
    public static function factory($uri) {
    
        $path = '';
        $pathinfo = '';
        if (is_array($uri)) {
            // @todo Maybe an exception should be thrown if there are missing required parts
            $parts = array();
            $parts['scheme'] = array_key_exists('scheme', $uri) ? $uri['scheme'] : null;
            $parts['host'] = array_key_exists('host', $uri) ? $uri['host'] : null;
            $parts['port'] = array_key_exists('port', $uri) ? $uri['port'] : null;
            $parts['user'] = array_key_exists('user', $uri) ? $uri['user'] : null;
            $parts['pass'] = array_key_exists('pass', $uri) ? $uri['pass'] : null;
            $parts['query'] = array_key_exists('params', $uri) ? $uri['params'] : null;
            $path = array_key_exists('path', $uri) ? $uri['path'] : null;
            $pathinfo = array_key_exists('pathinfo', $uri) ? $uri['pathinfo'] : null;
            $parts['fragment'] = array_key_exists('fragment', $uri) ? $uri['fragment'] : null;
        } else {
            // if a URI string was passed in, parse it
            $parts = self::parse_url($uri);
            // pre($parts);
            if (array_key_exists('path', $parts) && strpos($parts['path'], '.')) {
                $path = trim($parts['path'], '/');
                $pathparts = explode('/', $path);
                $inpi = false;
                $path = array();
                $parthinfo = array();    
                foreach($pathparts as $part) {
                    if ($inpi) {
                        $pathinfo[] = $part;
                    } else {
                        $path[] = $part;
                    }
                    if (strpos($part, '.')) {
                        $inpi = true;
                    }
                }
                $path = implode('/', $path);
                $pathinfo = '/' . implode('/', $pathinfo) . '/';
                // $scheme = array_key_exists('')
                // return new OAuth_Uri(, );
            }
        }
        
        return new OAuth_Uri($parts['scheme'], $parts['host'], $path, $parts['query'], $parts['port'], $pathinfo, $parts['fragment'], $parts['user'], $parts['pass']);
    
    }
    
    /**
     * This method is exactly the same as PHP's parse_url function except that
     * it always returns all possible keys even if there is no corresponding value,
     * whereas the parse_url function only returns the keys who have values.
     */
    public static function parse_url($uri) {
    
        $parts = parse_url($uri);
        $defaults = array(
            'scheme' => null,
            'host' => null,
            'port' => null,
            'user' => null,
            'pass' => null,
            'path' => null,
            'query' => null,
            'fragment' => null,
        );
        return array_merge($defaults, $parts);
    
    }
    
    public function __toString() {
    
        $host = $this->getHostName();
        if ($user = $this->getUser()) {
            if ($pass = $this->getPassword()) {
                $host = "$user:$pass@$host";
            } else {
                $host = "$user@$host";
            }
        }
        $port = $this->getPort();
        $scheme = $this->getScheme();
        
        // @todo There is a definite code smell here...
        // once I add support for more schemes than just http and https,
        // I need to create classes for each scheme type and this logic needs to happen there
        switch($scheme) {
            case 'http':
                if ($port != 80) {
                    $host = "$host:$port";
                }
                break;
            case 'https':
                if ($port != 443) {
                    $host = "$host:$port";
                }
                break;
        }
        
        $url = "$scheme://$host";
        if ($path = $this->getPath()) {
            $url .= "/" . ltrim($path, "/");
        }
        if ($params = $this->getParams()) {
            $pairs = array();
            foreach ($params as $key => $val) {
                if (is_null($val)) {
                    $pairs[] = $key;
                } else {
                    $pairs[] = "$key=$val";
                }
            }
            $url .= "?" . implode('&', $pairs);
        }
        return $url;
    
    }

}